用 webpack 打包一个简单的 todo Demo

本次学习案例基于某客网的教学视频,这里记录一下加深自己的学习印象以及方便以后查找,感兴趣的童鞋可以自行观看视频:点这里

这里只讲 webpack 配置,所以直接贴配置代码就好啦,相关配置都有注释,如果参考学习的话可只看webpack.config.js:

目录结构:

项目目录

package.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"name": "vue-ssr-tech",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "cross-env NODE_ENV=production webpack --config webpack.config.js",
"dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"autoprefixer": "^9.1.0",
"babel-core": "6.26.0",
"babel-helper-vue-jsx-merge-props": "2.0.3",
"babel-loader": "7.1.2",
"babel-plugin-syntax-jsx": "6.18.0",
"babel-plugin-transform-vue-jsx": "3.5.0",
"babel-preset-env": "1.6.1",
"cross-env": "5.1.3",
"css-loader": "0.28.7",
"extract-text-webpack-plugin": "3.0.2",
"file-loader": "^1.1.11",
"html-webpack-plugin": "2.30.1",
"postcss-loader": "^2.1.6",
"style-loader": "^0.21.0",
"stylus": "0.54.5",
"stylus-loader": "3.0.1",
"url-loader": "^0.6.2",
"vue": "2.5.13",
"vue-loader": "13.6.0",
"vue-template-compiler": "2.5.13",
"webpack": "3.10.0",
"webpack-dev-server": "2.9.7"
}
}

webpack.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
const path = require('path')
const webpack = require('webpack')
const HTMLPlugin = require('html-webpack-plugin') //打包生成html必要插件
const ExtractPlugin = require('extract-text-webpack-plugin') //在打包文件中提取文本到一个单独的文件中插件

const isDev = process.env.NODE_ENV === 'development' //环境变量,由cross-env提供

const config = {
target: "web", //表示web项目(跑在浏览器)
entry: {
index: path.join(__dirname,'src/index.js')
},
output: {
filename: "bundle.[hash:8].js", //开发环境打包名,带 hash
path:path.join(__dirname,'dist')
},

module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.jsx$/,
loader: 'babel-loader' //vue里面对jsx支持
},
// {
// test:/\.css$/,
// use:[
// 'style-loader',
// 'css-loader'
// ]
// },
{
test:/\.(gif|jpg|jpeg|png|svg)$/,
use:[
{
loader:'url-loader',
options: {
limit: 1024,
name: '[name]-new.[ext]'
}
}
]
}
]
},
plugins: [
new webpack.DefinePlugin({ //webpack根据是开发环境还是生产环境进行打包框架代码(这里是vue)
'process.env': {
NODE_ENV: isDev ? '"development"' : '"production"'
}
}),
new HTMLPlugin(),
]
}

if(isDev){ //如果是生产环境
config.module.rules.push({ //样式编译在开发环境的使用,动态Push
test:/\.styl$/,
use:[
'style-loader',
'css-loader',
{
loader: 'postcss-loader', //配置postcss
options: {
sourceMap: true,//两者都会生产sourceMap,配置直接使用前者生成的sourceMap,加快编译效率
}
},
'stylus-loader'
]
})
//本来访问页面上的代码是经过压缩的,调试起来困难,加上这个配置,
//调试的时候我们可以调试未压缩打包的代码,而不是打包后的代码
config.devtool = '#cheap-module-eval-source-map'
config.devServer = {
port: 8000, //端口
host: '0.0.0.0', //这样设置可以通过localhost:127.0.0.1访问,也可以通过内网ip访问(手机和其它电脑)
overlay: {//设置webpack在进行编译的过程中,如果有错误则显示在网页上,方便定位错误
errors: true,
},
//historyFallback: {}, //改配置可以把其它地址映射到入口页面(主要用于单页面应用)
open: true, //每次自动打开浏览器
hot: true //开启热更新
}
config.plugins.push(
new webpack.HotModuleReplacementPlugin(), //开启热更新所需插件
new webpack.NoEmitOnErrorsPlugin() //减少不需要信息展示问题
)
}else{
config.entry = {
app: path.join(__dirname, 'src/index.js'),
vendor: ['vue'] //单独打包vue
}
config.output.filename = '[name].[chunkhash:8].js' //生产环境打包后文件名,带chunk
config.module.rules.push({ //样式编译在生产环境的使用
test: /\.styl/,
use: ExtractPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader',
{
loader: 'postcss-loader',
options: {
sourceMap: true,
}
},
'stylus-loader'
]
})
})
config.plugins.push(
new ExtractPlugin('styles.[contentHash:8].css'), //根据css内容得到单独的 hash
new webpack.optimize.CommonsChunkPlugin({ //单独打包vue
name: 'vendor'
}),
new webpack.optimize.CommonsChunkPlugin({ //未命名路口,将webpack生产的相关代码单独打包
name: 'runtime'
})
)
}


module.exports = config

postcss.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
const autoprefixer = require('autoprefixer')

module.exports = {
plugins: [
autoprefixer()
]
}

// module.exports = {
// plugins: [
// require('autoprefixer')
// ]
// }

.babelrc:

1
2
3
4
5
6
7
8
{
"presets": [
"env"
],
"plugins": [
"transform-vue-jsx"
]
}

其他代码顺便粘贴一下,可以不用看啦:

index.js:

1
2
3
4
5
6
7
8
9
10
11
import Vue from 'vue'
import App from './app.vue'

import './assets/styles/global.styl'

const root = document.createElement('div')
document.body.appendChild(root)

new Vue({
render:(h) => h(App)
}).$mount(root)

app.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<template>
<div id="app">
<div id="cover"></div>
<Header></Header>
<todo></todo>
<Footer></Footer>
</div>
</template>

<script>
import Header from './todo/header.vue'
import Footer from './todo/footer.jsx'
import Todo from './todo/todo.vue'

export default {
components: {
Header,
Footer,
Todo,
}
}
</script>

<style lang="stylus" scoped>
#app{
position absolute
left 0
right 0
top 0
bottom 0
}
#cover{
position absolute
left 0
top 0
right 0
bottom 0
background-color #999
opacity .9
z-index -1
}
</style>

header.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<header class="main-header">
<h1>JTodo</h1>
</header>
</template>

<style lang="stylus" scoped>
.main-header{
text-align center
h1{
font-size 100px
color: rgba(175, 47, 47, 0.4)
font-weight 100
margin 20px
}
}
</style>

item.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<template>
<div :class="['todo-item', todo.completed ? 'completed' : '']">
<input
type="checkbox"
class="toggle"
v-model="todo.completed"
>
<label>{{todo.content}}</label>
<button class="destory" @click="deleteTodo"></button>
</div>
</template>

<script>
export default {
props: {
todo: {
type: Object,
required: true,
}
},
methods: {
deleteTodo() {
this.$emit('del', this.todo.id)
}
}
}
</script>

<style lang="stylus" scoped>
.todo-item{
position relative
background-color #fff
font-size 24px
border-bottom 1px solid rgba(0,0,0,0.06)
&:hover{
.destory:after{
content: '×'
}
}
label{
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
&.completed{
label{
color: #d9d9d9;
text-decoration line-through
}
}
}
.toggle{
text-align: center;
width: 40px;
height: 40px;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none;
appearance: none;
outline none
&:after{
content url('../assets/images/round.svg')
}
&:checked:after{
content url('../assets/images/done.svg')
}
}
.destory{
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
background-color transparent
appearance none
border-width 0
cursor pointer
outline none
}
</style>

footer.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import '../assets/styles/footer.styl'

export default {
data() {
return {
author: 'Jokcy'
}
},
render() {
return (
<div id="footer">
<span>Written by {this.author}</span>
</div>
)
}
}

tabs.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<template>
<div class="helper">
<span class="left">{{unFinishedTodoLength}} items left</span>
<span class="tabs">
<!--key相同则复用节点-->
<span
v-for="state in states"
:key="state"
:class="[state, filter === state ? 'actived' : '']"
@click="toggleFilter(state)"
>
{{state}}
</span>
</span>
<span class="clear" @click="clearAllCompleted">Clear Completed</span>
</div>
</template>

<script>
export default {
props: {
filter: {
type: String,
required: true,
},
todos: {
type: Array,
required: true,
}
},
data() {
return {
states: ['all', 'active', 'completed']
}
},
computed: {
unFinishedTodoLength() {
return this.todos.filter(todo => !todo.completed).length
}
},
methods: {
clearAllCompleted() {
this.$emit('clearAllCompleted')
},
toggleFilter(state) {
this.$emit('toggle', state)
}
}
}
</script>

<style lang="stylus" scoped>
.helper{
font-weight 100
display flex
justify-content space-between
padding 5px 0
line-height 30px
background-color #fff
font-size 14px
font-smoothing: antialiased
}
.left, .clear, .tabs{
padding 0 10px
box-sizing border-box
}
.left, .clear{
width 150px
}
.left{
text-align left
}
.clear{
text-align right
cursor pointer
}
.tabs{
width 200px
display flex
justify-content space-around
* {
display inline-block
padding 0 10px
cursor pointer
border 1px solid rgba(175,47,47,0)
&.actived{
border-color rgba(175,47,47,0.4)
border-radius 5px
}
}
}
</style>

todo.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<template>
<section class="real-app">
<input
type="text"
class="add-input"
autofocus="autofocus"
placeholder="接下去要做什么?"
@keyup.enter="addTodo"
>
<item
:todo="todo"
v-for="todo in filteredTodos"
:key="todo.id"
@del="deleteTodo"
/>
<tabs
:filter="filter"
:todos="todos"
@toggle="toggleFilter"
@clearAllCompleted="clearAllCompleted"
/>
</section>
</template>

<script>
import Item from './item.vue'
import Tabs from './tabs.vue'
let id = 0
export default {
data() {
return {
todos: [],
filter: 'all'
}
},
components: {
Item,
Tabs,
},
computed: {
filteredTodos() {
if (this.filter === 'all') {
return this.todos
}
const completed = this.filter === 'completed'
return this.todos.filter(todo => completed === todo.completed)
}
},
methods: {
addTodo(e) {
this.todos.unshift({
id: id++,
content: e.target.value.trim(),
completed: false
})
e.target.value = ''
},
deleteTodo(id) {
this.todos.splice(this.todos.findIndex(todo => todo.id === id), 1)
},
toggleFilter(state) {
this.filter = state
},
clearAllCompleted() {
this.todos = this.todos.filter(todo => !todo.completed)
}
}
}
</script>

<style lang="stylus" scoped>
.real-app{
width 600px
margin 0 auto
box-shadow 0 0 5px #666
}
.add-input{
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
font-smoothing: antialiased;
padding: 16px 16px 16px 60px;
border: none;
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
</style>

footer.styl and global.styl:

1
2
3
4
5
6
7
#footer{
margin-top 40px
text-align center
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
html, body{
margin 0
padding 0
width 100%
height 100%
}
body{
background-image: url('../images/bg.jpeg')
background-size: cover
background-position: center;
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #4d4d4d;
font-smoothing: antialiased;
font-weight: 300;
}



完~